嗨,大家今天過得好嗎?歡迎來到鐵人賽 Day 7。
昨天我們成功用 Vibe Coding 把首頁打磨得很有科技感。不過,如果你仔細看昨天的 ContentView.swift,你會發現一個隱憂:所有的 UI 程式碼都塞在同一個檔案裡。
現在還只是一個輸入框和一個按鈕而已。想像一下,等我們之後串接了 n8n,還需要加入「等待 AI 產出結果的轉圈圈動畫 (Loading State)」、「錯誤提示訊息 (Error Alert)」,以及「最終菜單展示卡片 (Result Card)」。
如果讓 AI 繼續把這些東西全塞進 ContentView,不用到下禮拜,你的檔案就會飆破 500 行。到那時候,AI 每次閱讀跟修改都會開始「幻覺」,不小心刪掉你的按鈕或是搞壞你的版面。
為了不讓 AI 暴走,身為工程師的我們必須主動進行「元件化」。我們要把它拆成一個一個獨立的小積木 (Subviews),再回到主畫面組合起來。
但在 SwiftUI 裡,只要一拆分 View,馬上就會面臨一個死穴:資料狀態綁定 (State Binding)。
很多新手直接叫 AI:「幫我把輸入框拆成另一個檔案」,結果 AI 拆是拆了,但原本存在 Controller 裡的 requestData 狀態卻斷線了,導致你在子畫面輸入的數字,主畫面根本收不到。
要讓 AI 乖乖幫你拆元件又不斷線,你的 Prompt 不能只說「幫我拆開」,你必須明確告訴它**「用什麼方式傳遞資料」**。在我們目前的 MVC 架構下,最適合傳遞 HardwareRequest 型別資料的方式是 @Binding。
請對 AI 下這段明確的重構指令:
這是我目前的 ContentView.swift (附上昨天的程式碼)。
為了維持良好的架構,請幫我將「卡片區塊 (包含標題、預算輸入框、用途選擇器)」獨立抽離成一個名為 `InputCardView` 的 SwiftUI 元件。
【拆分要求】
1. 請在 `InputCardView` 中使用 `@Binding` 來接收 `HardwareRequest` 資料。
2. `ContentView` 必須保持乾淨,透過傳遞 `$controller.requestData` 給 `InputCardView` 來維持狀態綁定。
3. 原本的設計風格與漸層按鈕請保留在 `ContentView`,不要更動 `HomeController` 邏輯。
拿到指令後,AI 就不會亂猜你的意圖,而是精準地產出以下兩個乾淨的結構。
你看,這裡使用了 @Binding,代表這個 View 只是個「接收器」,它不負責保存資料,資料的源頭依然在 Controller 身上。
import SwiftUI
struct InputCardView: View {
// 透過 @Binding 與外部的 Controller 狀態保持同步
@Binding var requestData: HardwareRequest
let purposeOptions: [String]
var body: some View {
VStack(alignment: .leading, spacing: 20) {
Text("請輸入您的需求")
.font(.headline)
.foregroundColor(.gray)
TextField("輸入預算 (如: 30000)", text: $requestData.budget)
.keyboardType(.numberPad)
.padding()
.background(Color(.systemGray6))
.cornerRadius(10)
Picker("選擇用途", selection: $requestData.purpose) {
ForEach(purposeOptions, id: \.self) { option in
Text(option).tag(option)
}
}
.pickerStyle(SegmentedPickerStyle())
}
.padding()
.background(Color.white)
.cornerRadius(15)
.shadow(color: Color.black.opacity(0.05), radius: 10, x: 0, y: 5)
}
}
回到主畫面,原本落落長的排版不見了,取而代之的是一行乾淨俐落的 InputCardView 呼叫。
import SwiftUI
struct ContentView: View {
@StateObject private var controller = HomeController()
var body: some View {
NavigationView {
ZStack {
Color(.systemGray6).edgesIgnoringSafeArea(.all)
VStack(spacing: 25) {
// 呼叫拆分出去的子元件,並將狀態綁定進去
InputCardView(
requestData: $controller.requestData,
purposeOptions: controller.purposeOptions
)
Spacer()
Button(action: {
controller.submitRequest()
}) {
Text("產生推薦菜單")
.font(.headline)
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding()
.background(
LinearGradient(gradient: Gradient(colors: [Color.blue, Color.purple]), startPoint: .leading, endPoint: .trailing)
)
.cornerRadius(12)
.shadow(color: Color.purple.opacity(0.3), radius: 10, x: 0, y: 5)
}
.padding(.bottom, 20)
}
.padding()
}
.navigationTitle("AI 硬體配對")
}
}
}
把畫面元件化,是我們對抗「AI 亂改一通」最強大的防禦機制。未來當我們要新增功能時,只要叫 AI 去修改特定的 View 檔案,主畫面跟邏輯層就能安然無恙。
到今天為止,我們前端的「輸入介面」已經完全準備就緒了。但現在我們的 HomeController 按下送出後,只會印出一行 print("準備發送 API 請求..."),這可不行。
明天,我們要在前端把「API Service 層」建立起來,並用 Mock Data (假資料) 先讓畫面動起來,為下週正式串接 n8n 引擎做好暖身!
我們 Day 08 見!